曾經有人問說「只要會寫程式的人,都一定會定義 function 的啊!那我都用 function 寫程式,就是 functional programming 了嗎?」
不,並不是這樣的。
在物件導向的程式語言裡,認為資料結構及操作這種資料結構的行為,應該要放在同一個地方, 這就是物件的初衷。有了物件的定義 (class or prototype) 後,該物件擁有用來操作自身資料 (或稱狀態) 的行為,也就是函式,通常會被稱作方法 (method)。
方法一定是函式,但反過來就不一定成立了。例如在 JavaScript 這個有趣的語言中,函式是一級成員。不需要隸屬於一個物件,不一定要有名稱(匿名函式),可以當成其它函式的參數來傳遞,也可以做為回傳值。在其它的語言裡,常常用 lambda 這種名稱/方式來處理這種一級公民函式的概念。
lambda 這個字,來自數學上的 λ-calculus (lambda calculus)。在三零年代由 Alonzo Church 發表,是一套從數學邏輯中發展出來的變數綁定及替換規則。由此規則推出一個函數抽象化定義、函數應用及遞迴的形式系統。
所以 functional programming 裡的第一個字,並非程式語言裡的概念,而是數學上的函數 (mathematical function),也就是國中課本裡的:
這個型式系統透過後來的柯里-霍華德同構推導出寫(某些)程式跟證明數學是同一件事。這麼一來,已發展的相關數學理論與解法,都可以實現在程式上。反之也可以用寫程式的方式處理數學證明。如果有一天你開始學 Haskell,會發現大家都在講 monad, functor 這些範疇論的概念,那就要恭喜你踏入函數式編程的更高境界了。
=
究竟是什麼意思可能很多人聽到數學就退避三舍,這時候就要拿 Haskell 圈裡的慣例出來用:
其實這很簡單,不要想得太複雜。
-- 放大絕前的招式咏唱
開玩笑的,現在這個真的很簡單。在數學公式裡,當我們說 y = ax + b
時,那個等號並不是在學 C, Ruby , Python, JavaScript 時說的變數指派,而是說對一個收斂函數來說,當輸入的 x
變化時,函式的輸出 y
會總是相同的值。而這就是我們今天要探討的主題,pattern matching,模式比對。
=
: 不是指派,而是模式比對在 Elixir 中,你可以寫 a = 1
,看起來及運作起來都跟變數指派一模一樣,但其實它有完全不同的含義:
把等號左邊的模式,跟右邊的值比對看看。如果有辦法找到讓等式成立的情況,那就幫你綁定變數。
在 JavaScript ES6 中引進了 destruct assignment,解構賦值。有些人以為這就是 patter matching,但其實還差了一點,看看以下的範例:
var [a, a, b] = [1, 2, 3]
// a => 2
// b => 3
因為等號只是單純的指派, a
先被指派成 1
,緊接著又被指派成 2
。
而在 Erlang/Elixir 中,一樣的輸入,會發生這樣的結果:
[a, a, b] = [1, 2, 3]
#=> ** (MatchError) no match of right hand side value: [1,2,3]
Elixir 用這訊息告訴你,它找不到方法讓這個等式成真, a
不可能同時是 1
又是 2
。這次比對是失敗的。
若將右邊的值改成這樣:
[a, a, b] = [1, 1, 3]
# => [1, 1, 3]
讓這個等式成真的方法,就是讓 a
綁定成 1
,b
綁定成 3
。
那麼你就了解,串列的模式比對也可以這樣用:
[a, 3, b] = [1, 2, 3]
#=> ** (MatchError) no match of right hand side value: [1,2,3]
[a, 2, b] = [1, 2, 3]
#=> [1,2,3]
昨天安裝時,最後有一個小題目:
[head | tails] = [1, 2, 3, 4]
現在你應該可以猜到,這是串列比對中的切開首值及剩下的串列的方式,如同 LISP 中的 car
及 cdr
。更多關於串列的細節,會在後面的章節討論。
每個比對本身會是一個有回傳值的 expression,那麼可以一直比下去也是很合理的:
[h | t] = [1, b, c, d] = [1, 2, 3, 4]
#=> b = 2, c = 3, d = 4
#=> h = 1, t = [2, 3, 4]
只要最右邊能求出所有的值,中間的比對都成功,那麼就可以一次綁定多個變數了。
_
: 我不在乎這個在 pattern matching 時,常常會遇到你只想要確保一部份的值或是資料結構,而不在乎其它地方,就可以利用 _
來進行 pattern matching。 例如我只想要將串列中前兩個元素綁定成變數,後面有什麼干我 P 事,就可以這樣做:
[a, b | _] = [1, 2, 3, 4, 5]
#=> a = 1, b = 2
一個比對裡,可以使用多個 _
。如果覺得這樣讓程式很難讀,可以改成用底線開頭的變數,效果是一樣的。
[first, _second, third | _tails] = [1, 2, 3, 4, 5]
#=> first = 1, third = 3
#=> 雖然可以用 _second 及 _tails 拿到值,但是 elixir 會噴警告給你。
^
: 將變數求值進行比對當某個變數已經被綁定好值了,你接著想要用這個值來進行比對,可以使用 Pin operator: ^
a = 0
[^a, b, 2 | _tails] = [0, 1, 2, 3, 4]
#=> b = 1
=
是 pattern matching,若存在讓等式成真的解,就進行變數綁定MatchError
|
來比對串列的尾巴_
去敷衍它 (咦^
Pattern matching 是 Elixir/Erlang 語法裡最核心的部份。明天將會就這個主題繼續延伸,討論函式上的模式比對,以及遞迴。
Happy hacking! 明天見啦!